package br.edu.ifce.mflj.conectividade;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import br.edu.ifce.mflj.comunicacao.Pacote;
import br.edu.ifce.mflj.comunicacao.TipoPacote;
import br.edu.ifce.mflj.dados.Usuario;
import br.edu.ifce.mflj.exception.PacoteInvalidoException;

public class Servidor implements Runnable {
	private	ServerSocket					serverSocket;
	private Map<String, ObjectOutputStream> clientesConectados;
	private List<Usuario>					listaDeUsuarios;

	public Servidor( Integer numeroPorta ){
		try {
			serverSocket			= new ServerSocket( numeroPorta );
			clientesConectados		= new HashMap<String, ObjectOutputStream>();
			listaDeUsuarios			= new ArrayList<Usuario>();

		} catch( IOException ioException ){}
	}

	public void run(){
		Socket				clienteSocket	= null;
		ObjectInputStream	streamDeEntrada	= null;
		Pacote				pacoteRecebido;

		try {
			System.out.println("Esperando cliente...");
			clienteSocket = serverSocket.accept();
			System.out.println("Novo cliente conectado.");

			new Thread( this ).start();

			streamDeEntrada	= new ObjectInputStream( clienteSocket.getInputStream() );
			pacoteRecebido	= lerProximoPacote( streamDeEntrada );

			/*
			 * Normalmente, o primeiro pacote a ser recebido por um cliente rec�m conectado �
			 * do tipo CHECK_IN, usado pelo cliente para se registrar. Nesse caso seu identificador,
			 * que est� guardado no payload do pacote, ser� usado como chave de acesso dentro do
			 * Map clientesConectados. Essa chave ser� usada para selecionar o socket do
			 * cliente destino de um poss�vel pacote do tipo MOVIMENTO. O processamento dos
			 * pacotes do cliente s� proceder� se o primeiro pacote for do tipo CHECK_IN.
			 */
			if( clienteChegandoAgora( pacoteRecebido ) ){
				registrarClienteNovo( clienteSocket, pacoteRecebido );

				transmitirClientesConectados( pacoteRecebido );

				while( !clienteIndoEmBoaHora( pacoteRecebido ) ){
					pacoteRecebido	= lerProximoPacote( streamDeEntrada );

					// Verificar se o tipo de pacote n�o � CHECK_OUT para n�o enviar desnecessariamente
					if( !clienteIndoEmBoaHora( pacoteRecebido ) ){
						encaminharPacoteAoDestinatario( pacoteRecebido );
					}
				}
			}
		}
		catch( IOException ioException ){
			ioException.printStackTrace();
		}
		catch( ClassNotFoundException classNotFoundException ){}
		catch( PacoteInvalidoException pacoteInvalidoException ){}
		finally{
			try {
				clienteSocket.close();

			}
			catch( NullPointerException nullPointerException ){}
			catch( IOException ioException ){
				ioException.printStackTrace();
			}

			try {
				streamDeEntrada.close();
				
			}
			catch( NullPointerException nullPointerException ){}
			catch( IOException ioException ){}

			try {
				serverSocket.close();
			}
			catch( NullPointerException nullPointerException ){}
			catch( IOException ioException ){}

		}
	}

	private Pacote lerProximoPacote( ObjectInputStream streamDeEntrada ) throws IOException, ClassNotFoundException {
		return (Pacote) streamDeEntrada.readObject();
	}

	private void registrarClienteNovo( Socket clienteSocket, Pacote pacoteRecebido ) throws IOException {
		clientesConectados.put( pacoteRecebido.getIdentificadorDoRemetente(), new ObjectOutputStream( clienteSocket.getOutputStream() ) );
		listaDeUsuarios.add( new Usuario( ((Usuario)pacoteRecebido.getPayload()).getApelido(), pacoteRecebido.getIdentificadorDoRemetente() ) );
	}

	private boolean clienteIndoEmBoaHora(Pacote pacoteRecebido) {
		return pacoteRecebido.getTipoPacote().equals( TipoPacote.CHECK_OUT );
	}

	private boolean clienteChegandoAgora( Pacote pacoteDeEntrada ){
		return pacoteDeEntrada.getTipoPacote().equals( TipoPacote.CHECK_IN );
	}

	private void encaminharPacoteAoDestinatario( Pacote pacote ) throws IOException {
		// Obtem do mapa de sockets dos clientes aquele para onde est� endere�ado o pacote, segundo o identificador do destinat�rio
		ObjectOutputStream streamDeSaida = clientesConectados.get( pacote.getIdentificadorDoDestinatario() );
		streamDeSaida.writeObject( pacote );
	}

	private void transmitirClientesConectados( Pacote pacote ) throws IOException, PacoteInvalidoException {
		for( Map.Entry<String, ObjectOutputStream> itemAtual : clientesConectados.entrySet()) {  
			String				identificadorAtual	= itemAtual.getKey();  
			ObjectOutputStream	streamDeSaida		= clientesConectados.get( identificadorAtual );

			// Através da streamDeSaida vai ser enviado uma série de pacotes: um para cada identificador registrado
			for( Usuario usuarioRegistrado : listaDeUsuarios ){
				System.out.println( "CHECK_IN de " + usuarioRegistrado.getApelido() + " para " + usuarioRegistrado.getIdentificador() ) ;
				streamDeSaida.writeObject( new Pacote( TipoPacote.CHECK_IN, new Usuario( usuarioRegistrado.getApelido(), usuarioRegistrado.getIdentificador() ), usuarioRegistrado.getIdentificador(), null ) );
			}
		}
	}

	public static void main(String[] args) {
		new Thread( new Servidor( 4444 ) ).start();
	}
}
